package ctu.kfad.run.memory.garbagecollector;

import ctu.kfad.run.InstanceController;
import ctu.kfad.run.InstanceDescriptor;
import ctu.kfad.run.ObjectTable;
import ctu.kfad.run.memory.*;

import java.util.List;

public class GarbageCollector {


    private final ObjectTable objectTable;
    private final IStackObjectProvider stackObjectProvider;
    private final MemoryManager memoryManager;
    private final ClassSizeDescriptorRepository descriptorRepository;

    public GarbageCollector(ObjectTable objectTable, IStackObjectProvider stackObjectProvider, MemoryManager memoryManager, ClassSizeDescriptorRepository descriptorRepository) {
        this.objectTable = objectTable;
        this.stackObjectProvider = stackObjectProvider;
        this.memoryManager = memoryManager;
        this.descriptorRepository = descriptorRepository;
    }

    public void collect() {
        markShouldLive();
        markDeleted();
        copyLive();
        memoryManager.cleanMemory();
    }

    private void copyLive() {
        memoryManager.swapMemory();

        final InstanceDescriptor[] descriptors = objectTable.getDescriptors();
        for (InstanceDescriptor descriptor : descriptors) {
            if (descriptor != null) {
                if (descriptor.isLive()) {
                    final int referenceToMemory = descriptor.getReferenceToMemory();
                    final String className = descriptor.getClassName();
                    final int totalSize;
                    if (className.equals(InstanceController.JAVA_REFERENCE_ARRAY)) {
                        final ArraySizeDescriptor arrayDescriptor = descriptor.getArrayDescriptor();
                        totalSize = arrayDescriptor.getSizePerItem() * arrayDescriptor.getLength();
                    } else if (className.equals(InstanceController.JAVA_STRING)) {
                        totalSize = descriptor.getArrayDescriptor().getLengthInBytes();
                    } else {
                        totalSize = descriptorRepository.getTotalSize(className);
                    }
                    descriptor.setReferenceToMemory(memoryManager.copyDataFromOldMemory(referenceToMemory, totalSize));
                }
            }
        }
    }

    private void markDeleted() {
        final InstanceDescriptor[] descriptors = objectTable.getDescriptors();
        for (InstanceDescriptor descriptor : descriptors) {
            if (descriptor != null) {
                if (!descriptor.isShouldLive()) {
                    descriptor.setLive(false);
                }
            }
        }
    }

    private void markShouldLive() {
        final InstanceDescriptor[] descriptors = objectTable.getDescriptors();
        for (InstanceDescriptor descriptor : descriptors) {
            if (descriptor != null) {
                descriptor.setShouldLive(false);
            }
        }

        final SurviveInstanceCollector surviveInstanceCollector = new SurviveInstanceCollector();
        final List<InstanceDescriptor> instanceDescriptors = surviveInstanceCollector.collectFromStackFrames(stackObjectProvider, objectTable);

        for (InstanceDescriptor instanceDescriptor : instanceDescriptors) {
            if (instanceDescriptor != null) {
                instanceDescriptor.setShouldLive(true);
            }
        }

        boolean someOneWhoShouldLiveIsUnInspected = true;
        while (someOneWhoShouldLiveIsUnInspected) {
            someOneWhoShouldLiveIsUnInspected = false;
            for (InstanceDescriptor descriptor : descriptors) {
                if (descriptor != null) {
                    if (descriptor.isLive() && descriptor.isShouldLive() && !descriptor.isInspected()) {
                        someOneWhoShouldLiveIsUnInspected = true;
                        final String className = descriptor.getClassName();
                        if (className.equals(InstanceController.JAVA_REFERENCE_ARRAY)) {
                            inspectForReferenceArray(descriptor);
                        } else if (className.equals(InstanceController.JAVA_STRING)) {
                            descriptor.setInspected(true);
                        } else {
                            inspectForClass(descriptor);
                        }
                    }
                }
            }
        }
    }

    private void inspectForReferenceArray(InstanceDescriptor descriptor) {
        final int referenceToMemory = descriptor.getReferenceToMemory();
        final int length = descriptor.getArrayDescriptor().getLength();
        for (int i = 0; i < length; i++) {
            final byte[] bytes = memoryManager.readMemory(referenceToMemory + i * FieldValueConverter.REFERENCE_BYTE_SIZE, FieldValueConverter.REFERENCE_BYTE_SIZE);
            final int address = FieldValueConverter.toInt(bytes);
            if (address != 0) {
                final InstanceDescriptor instanceDescriptor = objectTable.getDescriptorForAddress(address);
                instanceDescriptor.setShouldLive(true);
            }
        }
        descriptor.setInspected(true);
    }

    private void inspectForClass(InstanceDescriptor descriptor) {
        final String className = descriptor.getClassName();
        final int referenceToMemory = descriptor.getReferenceToMemory();
        final int[] addressesForReferences = descriptorRepository.getAddressesForReferences(className);
        for (int addressesForReference : addressesForReferences) {
            final byte[] bytes = memoryManager.readMemory(referenceToMemory + addressesForReference, FieldValueConverter.REFERENCE_BYTE_SIZE);
            final int address = FieldValueConverter.toInt(bytes);
            if (address != 0) {
                final InstanceDescriptor instanceDescriptor = objectTable.getDescriptorForAddress(address);
                instanceDescriptor.setShouldLive(true);
            }
        }
        descriptor.setInspected(true);
    }


}
